package com.mangix.db;

import com.mangix.conf.MangixConfig;
import com.mangix.core.Finger;
import com.mangix.core.MangixNode;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

/**
 * Created by ZXL on 2014/5/13.
 */
public class MangixDB {

    private Finger[] finger;
    private MangixNode predecessor;
    private int m;
    private int port;
    private String fingerFileDir;

    private MangixConfig conf;

    public MangixDB(MangixConfig conf, int port) throws Exception {
        this.conf = conf;
        this.port = port;
        m = conf.getM();
        String DBDir = conf.getDBDir();
        Path dbPath = Paths.get(DBDir);
        if (!Files.exists(dbPath))
            Files.createDirectories(dbPath);
        File fingerFile = new File(DBDir, "db_" + port + ".json");
        fingerFileDir = fingerFile.getPath();
    }

    /**
     * Init the db of the mangix node, including its finger table and processor.
     * @param local
     * @throws Exception
     */
    public void init(MangixNode local) throws Exception {
        File fingerFile = new File(conf.getDBDir(), "db_" + port + ".json");
        if (fingerFile.exists()) {
            Path dbFile = Paths.get(conf.getDBDir(), "db_" + port + ".json");
            String dbContent = new String(Files.readAllBytes(dbFile));
            Map dataJson = (Map) JSONValue.parse(dbContent);
            Map predecessorJson = (Map) dataJson.get("predecessor");
            List fingerJson = (List) dataJson.get("finger");
            if (fingerJson.size() != m) {
                // The db file is too old, because size of the finger table is not the same with current M.
                // So we delete the db file and createLocalNodes a new one.
                System.out.println("The db file is too old, because size of the finger table is not the same with current M. So we delete the db file and createLocalNodes a new one.");
                Path dbPath = Paths.get(fingerFile.getAbsolutePath());
                Files.delete(dbPath);
                this.initFingerTable(local, false);
                return;
            }
            predecessor = MangixNode.createNode(predecessorJson.get("host_name").toString(), Integer.parseInt(predecessorJson.get("port").toString()));
//            List fingerJson = (List) dataJson.get("finger");
            finger = new Finger[m];
            long currentId = local.hash();
            long ringSize = (long)Math.pow(2, local.getConfig().getM());
            for (int i = 0; i < m; i++) {
                Map oneFingerJson = (Map) fingerJson.get(i);
                Map oneNodeJson = (Map) oneFingerJson.get("node");
                MangixNode node = MangixNode.createNode(oneNodeJson.get("host_name").toString(), Integer.parseInt(oneNodeJson.get("port").toString()));
                finger[i] = new Finger((currentId + (long)Math.pow(2, i)) % ringSize, node);
            }
            this.flushDBFile();
        } else {
            this.initFingerTable(local, false);
        }
    }

    public synchronized void initFingerTable(MangixNode local, boolean isClear) throws Exception {
        finger = new Finger[m];
        if (isClear) {
            finger[0] = new Finger((local.hash() + 1) & ((1 << m) - 1), null);
            predecessor = null;
            for (int i = 1; i < m; i++)
                finger[i] = new Finger((finger[i - 1].start + (1 << (i - 1))) & ((1 << m) - 1), null);
        } else {
            finger[0] = new Finger((local.hash() + 1) & ((1 << m) - 1), local);
            predecessor = local;
            for (int i = 1; i < m; i++)
                finger[i] = new Finger((finger[i - 1].start + (1 << (i - 1))) & ((1 << m) - 1), local);
        }
        this.flushDBFile();
    }

    public synchronized void flushDBFile() throws Exception {
        JSONObject ret = new JSONObject();
        JSONArray fingerJson = new JSONArray();
        for (int i = 0; i < m; i++) {
            JSONObject oneFingerJson = new JSONObject();
            JSONObject oneNodeJson = new JSONObject();
            oneFingerJson.put("start", finger[i].start);
            if (finger[i].node == null) {
                oneNodeJson.put("host_name", "null");
                oneNodeJson.put("port", 0);
            } else {
                oneNodeJson.put("host_name", finger[i].node.getHostName());
                oneNodeJson.put("port", finger[i].node.getPort());
            }
            oneFingerJson.put("node", oneNodeJson);
            fingerJson.add(oneFingerJson);
        }
        ret.put("finger", fingerJson);
        JSONObject predecessorJson = new JSONObject();
        if (predecessor == null) {
            predecessorJson.put("host_name", "null");
            predecessorJson.put("port", 0);
        } else {
            predecessorJson.put("host_name", predecessor.getHostName());
            predecessorJson.put("port", predecessor.getPort());
        }
        ret.put("predecessor", predecessorJson);
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(fingerFileDir);
            fileOutputStream.write(ret.toString().getBytes());
        } finally {
            if (fileOutputStream != null) fileOutputStream.close();
        }
    }

    public synchronized Finger[] getFingerTable() {
        return finger;
    }

    public Finger getFinger(int pos) {
        return finger[pos];
    }

    public synchronized void updateFingerTable(int i, MangixNode node) throws Exception {
        finger[i].node = node;
        flushDBFile();
    }

    public synchronized void deleteFingerTable() {
    }

    public synchronized void updatePredecessor(MangixNode node) throws Exception {
        predecessor = node;
        flushDBFile();
    }

    public synchronized MangixNode getPredecessor() {
        return predecessor;
    }

    public synchronized MangixNode getSuccessor() {
        return finger[0].node;
    }
}
